"""
@File    :   service.py
@Time    :   2024/03/15 16:36:14
@Author  :   Qrj
@Email   :   923207736@qq.com
@description   :   OAuth2授权码模式认证相关业务逻辑
"""

from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession

from src.business.admin.system.oauth2.auth.enums import OAuth2GrantTypeEnum
from src.business.admin.system.oauth2.auth.password.service import PasswordService
from src.business.admin.system.oauth2.client.schemas.out import (
    Oauth2ClientCompleteOutSchema,
)
from src.business.admin.system.oauth2.client.service import Oauth2ClientService
from src.business.admin.system.oauth2.token.schemas.out import (
    DoubleTokenCompleteOutSchema,
)
from src.business.admin.system.oauth2.token.schemas.validate import (
    DoubleTokenCreateValidateSchema,
)
from src.business.admin.system.oauth2.token.service import TokenService
from src.business.admin.system.tenant.tenant.schemas.out import TenantCompleteOutSchema
from src.business.admin.system.tenant.tenant.service import TenantService
from src.business.admin.system.user.schemas.out import (
    UserCompleteAndHavePasswordHashOutSchema,
)
from src.business.admin.system.user.service import UserService
from src.exceptions.custom_async_validator import generate_async_validator_exception
from src.utils.cashews import data_cache
from src.utils.uri import UriUtil
from src.utils.uuid import UuidUtil

from .schemas.out import AuthorizationCodeCompleteOutSchema
from .schemas.validate import (
    AuthorizationCodeCreateDoubleTokenValidateSchema,
    AuthorizationCodeCreateValidateSchema,
)


class AuthorizationCodeService:
    """OAuth2授权码模式认证业务逻辑类"""

    @staticmethod
    def generate_auth_code_key(code: str | None = None) -> tuple[str, str]:
        """生成授权码及授权码在缓存中的key

        Args:
            code (str | None): 授权码

        Returns:
            tuple[str, str]: 授权码的缓存key, 授权码
        """
        # 如果没有传入code, 则自动生成code
        if code is None:
            code: str = UuidUtil.generate_uuid1()
        return f"authorization_code:{code}", code

    @staticmethod
    async def create_authorization_code(
        db_session: AsyncSession, create_schema: AuthorizationCodeCreateValidateSchema
    ):
        """创建授权码并写入缓存中

        Args:
            db_session (AsyncSession): 数据库连接池
            create_schema (AuthorizationCodeCreateValidateSchema): 创建所需的信息
        """
        # 验证租户是否存在
        tenant: TenantCompleteOutSchema = await TenantService.get_tenant_use_id(
            db_session, create_schema.tenant_id
        )
        # 验证OAuth2客户端是否存在
        oauth2_client: Oauth2ClientCompleteOutSchema = (
            await Oauth2ClientService.get_oauth2_client_use_client_id(
                db_session,
                create_schema.tenant_id,
                create_schema.oauth2_client_client_id,
            )
        )
        # 验证是否允许客户端使用当前授权模式
        Oauth2ClientService.validate_oauth2_client_authorized_grant_type(
            oauth2_client.authorized_grant_types, OAuth2GrantTypeEnum.authorization_code
        )
        # 验证客户端的重定向uri是否在允许的可重定向的URI地址列表内
        Oauth2ClientService.validate_oauth2_client_redirect_uri(
            oauth2_client, str(create_schema.redirect_uri)
        )
        # 获取用户信息
        user: UserCompleteAndHavePasswordHashOutSchema = (
            await UserService.get_user_use_username(
                db_session,
                tenant.id,
                username=create_schema.username,
                need_password_hash=True,
            )
        )
        # 密码密文解密
        password = await PasswordService.user_login_password_decrypt(
            create_schema.password, create_schema.encrypt_type
        )
        # 验证用户登录
        await PasswordService.user_login(user.password_hash, password)
        # 生成授权范围(自动赋予的oauth2授权范围 + 用户选择的在oauth2授权范围的权限)
        scopes = Oauth2ClientService.generate_scopes(
            oauth2_client.scopes,
            oauth2_client.auto_approve_scopes,
            create_schema.required_scopes,
        )
        # 将授权码数据写入缓存
        # 生成授权码及其缓存key
        code_key, code = AuthorizationCodeService.generate_auth_code_key()
        # 构建出携带query参数的重定向uri
        include_query_redirect_uri = UriUtil.add_query_to_url(
            str(create_schema.redirect_uri),
            {
                "code": code,
                "state": create_schema.state,
                "tenant_id": tenant.id,
            },
        )
        # 构建授权码数据
        authorization_code_data = AuthorizationCodeCompleteOutSchema(
            code=code,
            state=create_schema.state,
            redirect_uri=create_schema.redirect_uri,
            tenant=tenant,
            oauth2_client=oauth2_client,
            user=user,
            scopes=scopes,
            include_query_redirect_uri=include_query_redirect_uri,
        )
        # 将授权码数据写入缓存
        await data_cache.set(
            code_key,
            authorization_code_data,
            expire=oauth2_client.authorization_code_expiration_seconds,
        )
        return authorization_code_data

    @staticmethod
    async def get_and_delete_authorization_code(
        code: str,
    ) -> AuthorizationCodeCompleteOutSchema:
        """获取并删除授权码缓存

        Args:
            code (str): 授权码

        Returns:
            AuthorizationCodeCompleteOutSchema: 授权码数据
        """
        # 生成授权码及其缓存key
        code_key, _ = AuthorizationCodeService.generate_auth_code_key(code)
        # 获取缓存key对应的授权码数据
        authorization_code_data = await data_cache.get(code_key)
        if authorization_code_data is None:
            # 授权码不存在则直接返回错误响应
            raise generate_async_validator_exception("code", "授权码不存在")
        else:
            # 如果存在, 则删除缓存
            await data_cache.delete(code_key)
        return authorization_code_data

    @staticmethod
    async def create_authorization_code_double_token(
        db_session: AsyncSession,
        token_redis_pool: Redis,
        create_schema: AuthorizationCodeCreateDoubleTokenValidateSchema,
    ) -> DoubleTokenCompleteOutSchema:
        """创建授权码模式的双Token

        Args:
            db_session (AsyncSession): 数据库连接池
            token_redis_pool (Redis): Token的redis缓存连接池
            create_schema (AuthorizationCodeCreateDoubleTokenValidateSchema): 创建所需的参数

        Returns:
            DoubleTokenCompleteOutSchema: 双Token数据
        """
        # 使用授权码从缓存出取出并删除授权码数据(一个授权码只能用一次)
        auth_code_data = (
            await AuthorizationCodeService.get_and_delete_authorization_code(
                create_schema.code
            )
        )
        # 验证传入的oauth2客户端ID是否正确
        if (
            create_schema.oauth2_client_client_id
            != auth_code_data.oauth2_client.client_id
        ):
            raise generate_async_validator_exception("client_id", "OAuth2客户端ID错误")
        # 验证传入的oauth2客户端密钥是否正确
        if (
            create_schema.oauth2_client_client_secret
            != auth_code_data.oauth2_client.client_secret
        ):
            raise generate_async_validator_exception(
                "client_secret", "OAuth2客户端密钥错误"
            )
        # 授权码模式的双Token有用户信息, 不占用用户的登录名额
        create_double_token_schema = DoubleTokenCreateValidateSchema(
            tenant_id=auth_code_data.tenant.id,
            tenant_is_system=auth_code_data.tenant.is_system,
            tenant_pakcage_id=auth_code_data.tenant.tenant_package_id,
            oauth2_client_id=auth_code_data.oauth2_client.id,
            oauth2_grant_type=OAuth2GrantTypeEnum.authorization_code.value,
            access_token_expiration_seconds=auth_code_data.oauth2_client.access_token_expiration_seconds,
            refresh_token_expiration_seconds=auth_code_data.oauth2_client.refresh_token_expiration_seconds,
            oauth2_scopes=auth_code_data.scopes,
            occupation_of_login_quota=False,
            user_id=auth_code_data.user.id,
            username=auth_code_data.user.username,
            user_is_admin=auth_code_data.user.is_admin,
            user_number_of_simultaneous_logins=auth_code_data.user.number_of_simultaneous_logins,
        )
        double_token = await TokenService.create_double_token(
            db_session, token_redis_pool, create_double_token_schema
        )
        return double_token
